﻿using System;
using System.Linq;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Discounts;
using Nop.Core.Domain.Orders;
using Nop.Plugin.Pricing.PreciousMetals.Domain;
using Nop.Services.Catalog;
using Nop.Services.Directory;
using Nop.Services.Discounts;

namespace Nop.Plugin.Pricing.PreciousMetals.Services
{
    public class PreciousMetalsPriceCalculationService : PriceCalculationService
    {
        private readonly IPreciousMetalsDetailService _preciousMetalsDetailService;
        private readonly IPreciousMetalsQuoteService _preciousMetalsQuoteService;
        private readonly IMeasureService _measureService;

        public PreciousMetalsPriceCalculationService(
            IWorkContext workContext,
            IDiscountService discountService,
            ICategoryService categoryService,
            IProductAttributeParser productAttributeParser,
            ShoppingCartSettings shoppingCartSettings,
            CatalogSettings catalogSettings,
            IPreciousMetalsDetailService preciousMetalsDetailService,
            IPreciousMetalsQuoteService preciousMetalsQuoteService,
            IMeasureService measureService)
            : base(workContext, discountService, categoryService, productAttributeParser, shoppingCartSettings, catalogSettings)
        {
            _preciousMetalsDetailService = preciousMetalsDetailService;
            _preciousMetalsQuoteService = preciousMetalsQuoteService;
            _measureService = measureService;
        }

        public decimal GetMetalTierPrice(ProductVariant productVariant, Customer customer, int quantity, PreciousMetalsDetail detail)
        {
            // get the price per piece
            var piecePrice = GetMetalPiecePrice(detail);

            if (productVariant.TierPrices == null || productVariant.TierPrices.Count == 0 ||
                (PreciousMetalsTierPriceType)detail.TierPriceType == PreciousMetalsTierPriceType.DoNotUse)
                return piecePrice;

            var tierPrices = productVariant.TierPrices.OrderBy(tp => tp.Quantity).ToList().FilterForCustomer(customer);

            var previousQty = 1;
            var previousPrice = piecePrice;
            foreach (var tierPrice in tierPrices)
            {
                //check quantity
                if (quantity < tierPrice.Quantity)
                    continue;
                if (tierPrice.Quantity < previousQty)
                    continue;

                //save new price
                previousPrice = tierPrice.Price;
                previousQty = tierPrice.Quantity;
            }

            // If there is no matching tier price, return the default single piece price
            if (previousQty == 1)
                return piecePrice;

            // Calculate tier prices based on type
            switch ((PreciousMetalsTierPriceType)detail.TierPriceType)
            {
                case PreciousMetalsTierPriceType.Percentage:
                    return piecePrice - ((piecePrice / 100m) * previousPrice);
                case PreciousMetalsTierPriceType.PriceDiscount:
                    return piecePrice - previousPrice;
                case PreciousMetalsTierPriceType.DoNotUse:
                    return piecePrice;
                default:
                    throw new ArgumentException("Invalid Tier Price Type!");
            }
        }

        public override decimal GetFinalPrice(ProductVariant productVariant, Customer customer, decimal additionalCharge,
                                              bool includeDiscounts, int quantity)
        {
            // If the plugin is not installed, just use the regular pricing service
            if (!PreciousMetalsPricingProvider.IsPluginInstalled)
                return base.GetFinalPrice(productVariant, customer, additionalCharge, includeDiscounts, quantity);

            // get the PreciousMetals detail for this product
            var detail = this._preciousMetalsDetailService.Get(productVariant.Id);

            // if there is none, just use the regular pricing service
            if (detail == null)
                return base.GetFinalPrice(productVariant, customer, additionalCharge, includeDiscounts, quantity);

            // get the piece price
            var initialPrice = GetMetalTierPrice(productVariant, customer, quantity, detail);

            //discount + additional charge (just like the original PriceCalculationService)
            decimal result;
            if (includeDiscounts)
            {
                Discount appliedDiscount;
                var discountAmount = GetDiscountAmount(productVariant, customer, additionalCharge, quantity,
                                                       out appliedDiscount);
                result = initialPrice + additionalCharge - discountAmount;
            }
            else
            {
                result = initialPrice + additionalCharge;
            }
            if (result < decimal.Zero)
                result = decimal.Zero;

            // return the total price
            return result;
        }

        /// <summary>
        /// This will return the metal price for the product variant's weight in the primary shop's currency
        /// </summary>
        /// <param name="detail"></param>
        /// <returns></returns>
        public decimal GetMetalPiecePrice(PreciousMetalsDetail detail)
        {

            // get the quote for this metal
            var quote = _preciousMetalsQuoteService.GetQuote((PreciousMetalType)detail.MetalType);

            // determine the unit price
            decimal unitPrice;
            switch ((PreciousMetalsQuoteType)detail.QuoteType)
            {
                case PreciousMetalsQuoteType.Bid:
                    unitPrice = quote.Bid;
                    break;
                case PreciousMetalsQuoteType.Ask:
                    unitPrice = quote.Ask;
                    break;
                case PreciousMetalsQuoteType.Low:
                    unitPrice = quote.Low;
                    break;
                case PreciousMetalsQuoteType.High:
                    unitPrice = quote.High;
                    break;
                default:
                    throw new ArgumentException("Invalid Quote Type!");
            }

            var weight = detail.Weight;
            if (detail.WeightId > 0) // assume zero means no conversion
            {
                // the unit price is per troy ounce, so convert the detail weight to troy ounces.
                var weightFrom = _measureService.GetMeasureWeightById(detail.WeightId);
                var weightTo = _measureService.GetMeasureWeightBySystemKeyword("troyounce");

                weight = _measureService.ConvertWeight(detail.Weight, weightFrom, weightTo, false);
            }

            // the piece price starts from the unit price times the weight
            var piecePrice = unitPrice * weight;

            // figure the markup
            var markup = 0m;
            switch ((PreciousPriceCalculationType)detail.MathType)
            {
                case PreciousPriceCalculationType.AddFirstThenMultiply:
                    markup = ((piecePrice + detail.FlatMarkup) * detail.PercentMarkup / 100m);
                    break;
                case PreciousPriceCalculationType.MultiplyFirstThenAdd:
                    markup = (piecePrice * detail.PercentMarkup / 100m) + detail.FlatMarkup;
                    break;
                default:
                    goto case PreciousPriceCalculationType.MultiplyFirstThenAdd;
            }

            // add the markup to the piece price
            piecePrice += markup;


            if (detail.PriceRounding > 0)
            {
                switch ((PriceRoundingType)detail.PriceRoundingType)
                {
                    case PriceRoundingType.RoundDown:
                        piecePrice = Math.Floor(piecePrice / (decimal)detail.PriceRounding) * ((decimal)detail.PriceRounding);
                        break;
                    case PriceRoundingType.RoundUp:
                        piecePrice = Math.Ceiling(piecePrice / (decimal)detail.PriceRounding) * ((decimal)detail.PriceRounding);
                        break;
                }
            }


            // return the price, but nothing less than the LowerAmount
            return detail.LowerAmount > piecePrice ? detail.LowerAmount : piecePrice;
        }
    }
}